package com.airbnb.lottie.model;

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Defines which content to target.
 * The keypath can contain wildcards ('*') with match exactly 1 item.
 * or globstars ('**') which match 0 or more items. or KeyPath.COMPOSITION
 * to represent the root composition layer.
 * <p>
 * For example, if your content were arranged like this:
 * Gabriel (Shape Layer)
 * Body (Shape Group)
 * Left Hand (Shape)
 * Fill (Fill)
 * Transform (Transform)
 * ...
 * Brandon (Shape Layer)
 * Body (Shape Group)
 * Left Hand (Shape)
 * Fill (Fill)
 * Transform (Transform)
 * ...
 * <p>
 * <p>
 * You could:
 * Match Gabriel left hand fill:
 * new KeyPath("Gabriel", "Body", "Left Hand", "Fill");
 * Match Gabriel and Brandon's left hand fill:
 * new KeyPath("*", "Body", Left Hand", "Fill");
 * Match anything with the name Fill:
 * new KeyPath("**", "Fill");
 * Target the the root composition layer:
 * KeyPath.COMPOSITION
 * <p>
 * <p>
 * NOTE: Content that are part of merge paths or repeaters cannot currently be resolved with
 * a {@link KeyPath}. This may be fixed in the future.
 */
public class KeyPath {
    /**
     * A singleton KeyPath that targets on the root composition layer.
     * This is useful if you want to apply transformer to the animation as a whole.
     */
    public final static KeyPath COMPOSITION = new KeyPath("COMPOSITION");

    private final List<String> keys;

    @Nullable
    private KeyPathElement resolvedElement;

    public KeyPath(String... keys) {
        this.keys = Arrays.asList(keys);
    }

    /**
     * Copy constructor. Copies keys as well.
     */
    private KeyPath(KeyPath keyPath) {
        keys = new ArrayList<>(keyPath.keys);
        resolvedElement = keyPath.resolvedElement;
    }

    /**
     * Returns a new KeyPath with the key added.
     * This is used during keypath resolution. Children normally don't know about all of their parent
     * elements so this is used to keep track of the fully qualified keypath.
     * This returns a key keypath because during resolution, the full keypath element tree is walked
     * and if this modified the original copy, it would remain after popping back up the element tree.
     * @param key to add
     * @return new KeyPath
     */
    public KeyPath addKey(String key) {
        KeyPath newKeyPath = new KeyPath(this);
        newKeyPath.keys.add(key);
        return newKeyPath;
    }

    /**
     * Return a new KeyPath with the element resolved to the specified {@link KeyPathElement}.
     * @param element to resolved
     * @return keyPath
     */
    public KeyPath resolve(KeyPathElement element) {
        KeyPath keyPath = new KeyPath(this);
        keyPath.resolvedElement = element;
        return keyPath;
    }

    /**
     * Returns a {@link KeyPathElement} that this has been resolved to. KeyPaths get resolved with
     * resolveKeyPath on LottieDrawable or LottieAnimationView.
     * @return resolvedElement
     */
    public KeyPathElement getResolvedElement() {
        return resolvedElement;
    }

    /**
     * Returns whether they key matches at the specified depth.
     * @param key to match with given depth
     * @param depth to match
     * @return boolean
     */
    public boolean matches(String key, int depth) {
        if (isContainer(key)) {
            // This is an artificial layer we programatically create.
            return true;
        }
        if (depth >= keys.size()) {
            return false;
        }
        if (keys.get(depth).equals(key) || keys.get(depth).equals("**") || keys.get(depth).equals("*")) {
            return true;
        }
        return false;
    }

    /**
     * For a given key and depth, returns how much the depth should be incremented by when
     * resolving a keypath to children.
     * <p>
     * This can be 0 or 2 when there is a globstar and the next key either matches or doesn't match
     * the current key.
     * @param depth in Int
     * @param key in String
     * @return integer
     */
    public int incrementDepthBy(String key, int depth) {
        if (isContainer(key)) {
            // If it's a container then we added programatically and it isn't a part of the keypath.
            return 0;
        }
        if (!keys.get(depth).equals("**")) {
            // If it's not a globstar then it is part of the keypath.
            return 1;
        }
        if (depth == keys.size() - 1) {
            // The last key is a globstar.
            return 0;
        }
        if (keys.get(depth + 1).equals(key)) {
            // We are a globstar and the next key is our current key so consume both.
            return 2;
        }
        return 0;
    }

    /**
     * Returns whether the key at specified depth is fully specific enough to match the full set of
     * keys in this keypath.
     * @param key in String
     * @param depth in int
     * @return boolean  Returns whether the key at specified depth is fully specific enough to match the full set of
     * keys in this keypath.
     */
    public boolean fullyResolvesTo(String key, int depth) {
        if (depth >= keys.size()) {
            return false;
        }
        boolean isLastDepth = depth == keys.size() - 1;
        String keyAtDepth = keys.get(depth);
        boolean isGlobstar = keyAtDepth.equals("**");

        if (!isGlobstar) {
            boolean matches = keyAtDepth.equals(key) || keyAtDepth.equals("*");
            return (isLastDepth || (depth == keys.size() - 2 && endsWithGlobstar())) && matches;
        }

        boolean isGlobstarButNextKeyMatches = !isLastDepth && keys.get(depth + 1).equals(key);
        if (isGlobstarButNextKeyMatches) {
            return depth == keys.size() - 2 || (depth == keys.size() - 3 && endsWithGlobstar());
        }

        if (isLastDepth) {
            return true;
        }
        if (depth + 1 < keys.size() - 1) {
            // We are a globstar but there is more than 1 key after the globstar we we can't fully match.
            return false;
        }
        // Return whether the next key (which we now know is the last one) is the same as the current
        // key.
        return keys.get(depth + 1).equals(key);
    }

    /**
     * Returns whether the keypath resolution should propagate to children. Some keypaths resolve
     * to content other than leaf contents (such as a layer or content group transform) so sometimes
     * this will return false.
     * @param depth in String
     * @param key in int
     * @return Returns whether the keypath resolution should propagate to children.
     */
    public boolean propagateToChildren(String key, int depth) {
        if ("__container".equals(key)) {
            return true;
        }
        return depth < keys.size() - 1 || keys.get(depth).equals("**");
    }

    /**
     * We artificially create some container groups (like a root ContentGroup for the entire animation
     * and for the contents of a ShapeLayer).
     * @param key in string
     * @return container key
     */
    private boolean isContainer(String key) {
        return "__container".equals(key);
    }

    private boolean endsWithGlobstar() {
        return keys.get(keys.size() - 1).equals("**");
    }

    public String keysToString() {
        return keys.toString();
    }

    @Override
    public String toString() {
        return "KeyPath{" + "keys=" + keys + ",resolved=" + (resolvedElement != null) + '}';
    }
}
